// ml278
// Copyright (C) 2008 Bjørn Øivind Bjørnsen, Aleksander Gjermundsen, Knut Andre Tidemann, Thomas Hansen
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

#include <assert.h>
#include "planetaryplanet.h"
#include "planetaryzone.h"
#include "planetaryscenemanager.h"
#include "planetaryoptions.h"
#include "planetarypage.h"
#include "planetaryloadable.h"

#include "eventmanager.h"

#include <Ogre.h>
#include <vector>

namespace Ogre
{
	PlanetaryPlanet::PlanetaryPlanet(PlanetarySceneManager *mgr)
	{
		mSceneMgr = mgr;
		mSceneNode = NULL;
		mAtmosphere = NULL;

		mIsInit = false;
		mIsLoaded = false;
		mHasAtmosphere = false;

		mFramesSinceLastLoad = 0xffffffff;
	}

	PlanetaryPlanet::~PlanetaryPlanet()
	{
		if(mIsLoaded)
			unload();
		if(mIsInit)
			uninit();
	}

	/**
	 * Initialises the planet.
	 * Sets position, radius and mass, inits the different zones and the atmosphere if enabled.
	 * Does some preliminary calculations.
	*/
	void PlanetaryPlanet::init()
	{
		PlanetaryOptions *o = mSceneMgr->getOptions();

		mPosition = o->mPlanetPosition;
		mRadius = o->mPlanetRadius;
		// TODO put in configfile/calculate based on radius
		mMass = 9.9736e18;

		assert(!mIsInit);
		assert(mZones.empty());

		mZones.reserve(6);
		mZones.resize(6);

		// Create Scene Node
		if(!mSceneNode)
		{
			const String name = "Node::" + o->mPlanetName;
			mSceneNode = mSceneMgr->getRootSceneNode()->createChildSceneNode(name);
		}

		// Create the 6 zones
		PlanetaryZone *z;
		for(int i = 0; i < 6; i++)
		{
			z = new PlanetaryZone(mSceneMgr);
			z->init(i,this);

			mZones[i] = z;
		}
		
		Real top = (mRadius * (1.0f + ((0.5f) * o->mTerrainScale)));
		Real bot = (mRadius * (1.0f + ((-0.5f) * o->mTerrainScale)));
		
		mMaxAltitude = top - bot;
		mMinAltitude = bot - mRadius;
		
		// Set zone neightbours
		mZones[ZONE_UP]->setNeighbour(NORTH,mZones[ZONE_BACK]);
		mZones[ZONE_UP]->setNeighbour(SOUTH,mZones[ZONE_FRONT]);
		mZones[ZONE_UP]->setNeighbour(WEST,mZones[ZONE_LEFT]);
		mZones[ZONE_UP]->setNeighbour(EAST,mZones[ZONE_RIGHT]);
		
		mZones[ZONE_DOWN]->setNeighbour(NORTH,mZones[ZONE_FRONT]);
		mZones[ZONE_DOWN]->setNeighbour(SOUTH,mZones[ZONE_BACK]);
		mZones[ZONE_DOWN]->setNeighbour(WEST,mZones[ZONE_LEFT]);
		mZones[ZONE_DOWN]->setNeighbour(EAST,mZones[ZONE_RIGHT]);

		mZones[ZONE_FRONT]->setNeighbour(NORTH,mZones[ZONE_UP]);
		mZones[ZONE_FRONT]->setNeighbour(SOUTH,mZones[ZONE_DOWN]);
		mZones[ZONE_FRONT]->setNeighbour(WEST,mZones[ZONE_LEFT]);
		mZones[ZONE_FRONT]->setNeighbour(EAST,mZones[ZONE_RIGHT]);

		mZones[ZONE_BACK]->setNeighbour(NORTH,mZones[ZONE_DOWN]);
		mZones[ZONE_BACK]->setNeighbour(SOUTH,mZones[ZONE_UP]);
		mZones[ZONE_BACK]->setNeighbour(WEST,mZones[ZONE_LEFT]);
		mZones[ZONE_BACK]->setNeighbour(EAST,mZones[ZONE_RIGHT]);
		
		mZones[ZONE_LEFT]->setNeighbour(NORTH,mZones[ZONE_UP]);
		mZones[ZONE_LEFT]->setNeighbour(SOUTH,mZones[ZONE_DOWN]);
		mZones[ZONE_LEFT]->setNeighbour(WEST,mZones[ZONE_BACK]);
		mZones[ZONE_LEFT]->setNeighbour(EAST,mZones[ZONE_FRONT]);

		mZones[ZONE_RIGHT]->setNeighbour(NORTH,mZones[ZONE_UP]);
		mZones[ZONE_RIGHT]->setNeighbour(SOUTH,mZones[ZONE_DOWN]);
		mZones[ZONE_RIGHT]->setNeighbour(WEST,mZones[ZONE_FRONT]);
		mZones[ZONE_RIGHT]->setNeighbour(EAST,mZones[ZONE_BACK]);
		
		if(o->mAtmosphere)
		{
			mHasAtmosphere = true;
			
			mAtmosphere = new PlanetaryAtmosphere(mSceneMgr);
			mAtmosphere->init(this);
		}
		
		EventManager::getSingletonPtr()->addEventListener(this);
		
		mIsInit = true;
	}

	/**
	 * Loads the planet.
	 * Makes sure that the zones and the atmosphere (if enabled) is loaded
	*/
	void PlanetaryPlanet::load()
	{
		assert(!mIsLoaded);

		for(int i = 0; i < 6; i++)
		{
			mZones[i]->load();
		}
		
		if(mHasAtmosphere)
		{
			mAtmosphere->load();
		}

		mIsLoaded = true;
	}

	/**
	 * Unloads the planet.
	 * Makes sure that the different zones are unloaded, and unloads the atmosphere if it is enabled.
	*/
	void PlanetaryPlanet::unload()
	{
		assert(mIsLoaded);

		for(int i = 0; i < 6; i++)
		{
			mZones[i]->unload();
		}
		
		if(mHasAtmosphere)
		{
			mAtmosphere->unload();
		}
		mIsLoaded = false;
	}

	/**
	 * Un-initialises the planet.
	 * Un-initialises the different zones, un-initialises the atmosphere if enabled, deletes all of the zones
	 * and the atmosphere (if enabled) and unregistrers the EventListener.
	*/
	void PlanetaryPlanet::uninit()
	{
		assert(mIsInit);

		// Delete the 6 zones
		PlanetaryZone *z;
		for(int i = 0; i < 6; i++)
		{
			z = mZones[i];
			z->uninit();

			delete z;
			mZones[i] = NULL;
		}
		
		if(mHasAtmosphere)
		{
			mAtmosphere->uninit();
			delete mAtmosphere;
		}

		mZones.clear();
		
		// Clear the scene node
		mSceneNode->removeAndDestroyAllChildren();

		// remove event listener
		EventManager::getSingletonPtr()->removeEventListener(this);

		mIsInit = false;
	}
	
	/**
	 * Gives whether the planet has an atmosphere or not.
	 * @returns true if the planet has an atmosphere, false if not.
	*/
	bool PlanetaryPlanet::hasAtmosphere()
	{
		return mHasAtmosphere;
	}
	
	/**
	 * Overridden method from EventListener.
	 * Makes sure that the paging is up to date, and that the atmosphere shader parametres is
	 * set correctly if the planet has an atmosphere.
	*/
	bool PlanetaryPlanet::frameStarted(const FrameEvent &evt)
	{
		updatePaging(mSceneMgr->getActiveCamera());

		if(mHasAtmosphere)
			mAtmosphere->updateShader();

		return true;
	}
	
	/**
	 * Overridden method from EventListener.
	 * Goes through load and unload queues.
	*/
	bool PlanetaryPlanet::frameEnded(const FrameEvent &evt)
	{
		// Process loading and unloading
		processUnloadQueues();
		processLoadQueues();

		return true;
	}
	
	/**
	 * updates the paging for all zones
	*/
	void PlanetaryPlanet::updatePaging(Camera *cam)
	{
		assert(mIsInit);

		for(unsigned int i = 0; i < 6; i++)
		{
			mZones[i]->updatePaging(cam);
		}
	}

	/**
	 * Adds a PlanetaryLoadable to the back of the local load queue.
	 * @param loadable the loadable resource to attempt to load. Will be added to the back of the load-queue.
	*/
	void PlanetaryPlanet::addLoadableToLoadQueue(PlanetaryLoadable *loadable)
	{
		assert(!loadable->isLoaded());
		assert(!loadable->isLoading());
		mLoadQueue.push_back(loadable);
		loadable->setIsLoading(true);
	}

	/**
	 * Adds a PlanetaryLoadable to the front of the local load queue.
	 * @param loadable the loadable resource to attempt to load. Will be added to the front of the load-queue.
	 * @note mLoadQueue is a deque, so this is still a constant-time operation, do not worry.
	*/
	void PlanetaryPlanet::insertLoadableToLoadQueue(PlanetaryLoadable *loadable)
	{
		assert(!loadable->isLoaded());
		assert(!loadable->isLoading());
		mLoadQueue.push_front(loadable);
		loadable->setIsLoading(true);
	}

	/**
	 * Adds a PlanetaryLoadable to the unload queue.
	 * Performs checks to find out if the given loadable is in the process of loading, and handles deletion of these,
	 * in addition to those who have been completely loaded.
	 * @param loadable the PlanetaryLoadable to attempt to unload
	*/
	void PlanetaryPlanet::addLoadableToUnloadQueue(PlanetaryLoadable *loadable)
	{
		if(loadable->isLoading())
		{
			for(std::deque<PlanetaryLoadable *>::iterator itr = mLoadQueue.begin(); itr != mLoadQueue.end(); itr++)
			{
				if((*itr) == loadable)
				{
					loadable->setIsLoading(false);
					mLoadQueue.erase(itr);
					return;
				}
			}
		}
		mUnloadQueue.push_back(loadable);
		loadable->setIsUnloading(true);
	}

	/**
	 * Processes the load queue, and loads new Loadables unless the timeout is in effect.
	 * @note the loading frequency is determined by the configfile
	*/
	void PlanetaryPlanet::processLoadQueues()
	{
		// Make sure we don't overload the system by spamming loading operations
		if(mFramesSinceLastLoad < mSceneMgr->getOptions()->mPageLoadFrequency)
		{
			mFramesSinceLastLoad++;
			return;
		}

		if(mLoadQueue.empty())
			return;

		PlanetaryLoadable *p = *mLoadQueue.begin();
		p->load();
		p->setIsLoading(false);

		mLoadQueue.pop_front();
		mFramesSinceLastLoad = 0;
	}

	/**
	 * Processes the unload queue, and unloads Loadables when needed.
	*/
	void PlanetaryPlanet::processUnloadQueues()
	{
		if(mUnloadQueue.empty())
			return;

		PlanetaryLoadable *p = *mUnloadQueue.begin();
		p->unload();
		p->setIsUnloading(false);

		mUnloadQueue.pop_front();
	}
	
	/**
	 * Sets the ground material on all zones.
	 * @param mat a reference to the MaterialPtr to use as a material for the ground
	 * @note this is overridden by setAtmosphereMaterial() if the planet has an atmosphere at the moment.
	 * This, of course, is rather nasty, and should be fixed.
	*/
	void PlanetaryPlanet::setGroundMaterial(MaterialPtr &mat)
	{
		for(unsigned int i = 0; i < 6; i++)
		{
			mZones[i]->setMaterial(mat);
		}
	}
	
	/**
	 * Sets the material for the atmosphere.
	 * @param basename the basename to use when looking up materials. All the materials to be used by the atmosphere
	 * must adhere to a given format (basename + "_{sky,ground}From{Space,Atmosphere}).
	 * @note this also sets the materials for the ground (due to the shader being split into four parts - two for both
	 * ground and the atmosphere itself.
	*/
	void PlanetaryPlanet::setAtmosphereMaterial(String basename)
	{
		// TODO this sets the material for the ground too, not particularly nice atm
		if(mHasAtmosphere)
			mAtmosphere->setMaterial(basename);
	}
}


